import type { Metadata } from 'next'; import { notFound, permanentRedirect } from 'next/navigation'; import { Suspense } from 'react'; import { SectionSkeleton } from '@/components/satellite/skeleton'; import { Hero, TelemetryPanel } from '@/components/satellite/hero'; import { LiveMap } from '@/components/satellite/live-map'; import { Block, Head, Note } from '@/components/satellite/primitives'; import { EventsSection, HistorySection, IdentifiersSection, LaunchSection, MissionSection, OrbitSection, OwnershipSection, RegistrationSection, RelatedSection, SourcesSection } from '@/components/satellite/sections'; import { ViewBeacon } from '@/components/satellite/view-beacon'; import { Container } from '@/components/ui/section'; import { api, ApiError, safe } from '@/lib/api'; import { fmtDate, fmtInt } from '@/lib/format'; import { MISSION_LABELS, OBJECT_TYPE_LABELS, routes, SITE_NAME, SITE_URL } from '@/lib/site'; import type { SatelliteDetail } from '@/lib/types'; type Params = { params: Promise<{ slug: string }> }; async function load(slug: string): Promise { try { const res = await api.satellite(slug); return res.data; } catch (e) { if (e instanceof ApiError && e.notFound) notFound(); throw e; } } function describe(d: SatelliteDetail): string { const bits: string[] = []; bits.push(`${d.name} (NORAD ${d.norad_id ?? '—'}${d.cospar_id ? `, COSPAR ${d.cospar_id}` : ''}) is a${d.status === 'ACTIVE' ? 'n active' : ` ${d.status.toLowerCase()}`} ${(OBJECT_TYPE_LABELS[d.object_type] ?? d.object_type).toLowerCase()}`); if (d.orbit_class) bits.push(`in ${d.orbit_class}`); if (d.operator_name) bits.push(`operated by ${d.operator_name}`); if (d.launch_date) bits.push(`launched ${fmtDate(d.launch_date)}${d.launch_site_name ? ` from ${d.launch_site_name}` : ''}`); let s = bits.join(' ') + '.'; if (d.orbital_state) s += ` Perigee ${fmtInt(d.orbital_state.perigee_km)} km, apogee ${fmtInt(d.orbital_state.apogee_km)} km, inclination ${d.orbital_state.inclination.toFixed(2)}°.`; else if (d.decay_date) s += ` Decayed ${fmtDate(d.decay_date)}.`; s += ' Live position, orbital elements, history and sources on SatelliteIndex.'; return s; } export async function generateMetadata({ params }: Params): Promise { const { slug } = await params; const d = await safe(api.satellite(slug)); if (!d) return { title: 'Satellite', robots: { index: false } }; const s = d.data; const title = `${s.name} — Live Orbit, NORAD ${s.norad_id ?? '—'}`; const description = describe(s); const canonical = routes.satellite(s.slug); return { title, description, alternates: { canonical }, openGraph: { title, description, url: `${SITE_URL}${canonical}`, type: 'article', siteName: SITE_NAME }, twitter: { card: 'summary_large_image', title, description }, }; } /** Streams after the shell: the history endpoint is the slowest call and must never delay the redirect/404 decision. */ async function OrbitWithHistory({ d }: { d: SatelliteDetail }) { const history = d.norad_id !== null ? ((await safe(api.satelliteHistory(d.slug)))?.data ?? null) : null; return ; } export default async function SatellitePage({ params }: Params) { const { slug } = await params; const d = await load(slug); if (d.redirected_from || d.slug !== slug) permanentRedirect(routes.satellite(d.slug)); const ident = String(d.norad_id ?? d.slug); const hasElements = d.orbital_state !== null; const jsonLd = { '@context': 'https://schema.org', '@type': 'Thing', name: d.name, alternateName: d.aliases.map((a) => a.alias).filter((a) => a !== d.name), identifier: [ d.norad_id !== null ? { '@type': 'PropertyValue', propertyID: 'NORAD', value: String(d.norad_id) } : null, d.cospar_id ? { '@type': 'PropertyValue', propertyID: 'COSPAR', value: d.cospar_id } : null, ].filter(Boolean), url: `${SITE_URL}${routes.satellite(d.slug)}`, description: describe(d), additionalType: 'https://schema.org/Dataset', subjectOf: { '@type': 'Dataset', name: `${d.name} orbital elements`, description: 'Latest two-line element set and derived orbital parameters', license: 'https://celestrak.org/NORAD/documentation/', isAccessibleForFree: true, creator: d.sources.map((s) => ({ '@type': 'Organization', name: s.name })) }, }; return (